<?php

declare(strict_types=1);

namespace Zms\Validate\Rule;

use Zms\Unit\Is;

/**
 * 身份证号码验证类
 * 用于验证输入值是否为有效的中国身份证号码。
 */
class IdentityNumberValidate extends RuleAbstract
{
    /**
     * 默认错误消息
     * 当验证失败时，返回的错误信息模板。
     * @var string
     */
    protected $_error = '{label}不是有效的身份证号码';

    /**
     * @var bool
     * 是否严格模式
     */
    protected $_strict = true;

    /**
     * @describe 是否时严格模式
     * @user zlf <zms-pro@qq.com> 2025/6/27
     * @param bool $strict
     * @return $this
     */
    public function setStrict(bool $strict): IdentityNumberValidate
    {
        $this->_strict = $strict;
        return $this;
    }

    /**
     * 验证身份证号码
     * 使用正则表达式和校验算法验证输入值是否为有效的身份证号码。
     * 如果输入值为空且 `_noSkipping` 属性为 `true`，则直接返回 `true`。
     *
     * @param mixed $value 输入值
     * @param string $label 验证名（用于错误信息中）
     * @return bool 验证结果
     */
    public function validate($value, string $label = ''): bool
    {
        // 确保输入值是字符串类型
        if ($value && is_string($value) || is_numeric($value)) {
            $value = (string)$value;
            // 如果值为空且 `_noSkipping` 为 `true`，直接返回 `true`
            if (Is::empty($value) && $this->_noSkipping === true) {
                $this->_value = $value;
                return true;
            }
            //先开始基础验证
            if (!$this->baseVerifyIdCard($value)) {
                $this->addError($this->_error, $label);
                return false;
            }

            //是否需要严格校验
            if ($this->_strict) {//严格模式
                // 调用内部方法验证身份证号码
                if ($this->verifyIdCard($value)) {
                    $this->_value = $value;
                    return true;
                }
                $this->addError($this->_error, $label);
                return false;
            }
            //顺利通过
            $this->_value = $value;
            return true;
        }
        // 如果验证失败，添加错误信息并返回 `false`
        $this->addError($this->_error, $label);
        return false;
    }


    /**
     * @describe 基础验证
     * @user zlf <zms-pro@qq.com> 2025/9/10
     * @param string $idCard
     * @return bool
     */
    private function baseVerifyIdCard(string $idCard): bool
    {
        // 1. 基础格式校验：18位，前17位数字，第18位数字或X/x
        if (!preg_match('/^\d{17}[\dXx]$/', $idCard)) {
            return false;
        }
        // 2. 校验前2位省份代码是否合法
        $provinceCode = substr($idCard, 0, 2);
        $validProvinceCodes = [
            '11', '12', '13', '14', '15', '21', '22', '23',
            '31', '32', '33', '34', '35', '36', '37',
            '41', '42', '43', '44', '45', '46',
            '50', '51', '52', '53', '54',
            '61', '62', '63', '64', '65',
            '71',
            '81', '82'
        ];
        return in_array($provinceCode, $validProvinceCodes, true);
    }


    /**
     * 验证身份证号码的格式和校验位
     * 1. 使用正则表达式验证身份证号码的格式是否正确。
     * 2. 验证身份证号码的最后一位校验位是否正确。
     *
     * @param string $idCard 身份证号码
     * @return bool 验证结果
     */
    private function verifyIdCard(string $idCard): bool
    {
// 1. 基本格式检查：必须是18位，前17位为数字，最后一位是数字或X
        if (!preg_match('/^\d{17}[\dXx]$/', $idCard)) {
            return false;
        }

        // 2. 提取前17位 + 第18位校验码（统一转为大写）
        $idCard = strtoupper($idCard);
        $body = substr($idCard, 0, 17); // 前17位
        $checkBit = $idCard[17];        // 第18位

        // 3. 权重系数（固定）
        $weight = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];

        // 4. 计算加权和
        $sum = 0;
        for ($i = 0; $i < 17; $i++) {
            $sum += $body[$i] * $weight[$i];
        }

        // 5. 计算余数
        $mod = $sum % 11;

        // 6. 根据余数查表得到应得的校验码
        $checkCodeMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
        $expectedCheckBit = $checkCodeMap[$mod];

        // 7. 比较实际校验码与计算出的校验码
        return $checkBit === $expectedCheckBit;
    }
}
